package com.quantum.eventbus
{

	import com.quantum.util.ReflectionUtils;

	import com.quantum.logging.LogFactory;

	import flash.events.Event;

	import mx.logging.ILogger;
	import mx.utils.StringUtil;

	/**
	 * @private
	 *
	 * Represents a delegate for an event listener. It is used to validate or filter the
	 * invocations of the listener, or to transform the event into some other form of
	 * parameters for the listener method based on what it expects.
	 *
	 * <p>If an event contains an error and the target of the event notification has a generic
	 * error handler (i.e., a method that is tagged as [ErrorHandler]), then that method is
	 * used to report the error. In such a case, if the event/response handler takes only 2
	 * parameters, i.e., does not take error as a parameter, the event/response handler is
	 * skipped. In other words, if an event contains an error and the target has not
	 * subscribed for the error parameter, then partial results are ignored.
	 *
	 * <p>A response handler can receive either the response event itself, or between 2 to 4
	 * of these arguments: the properties of the response event that are labeled as [Request],
	 * [Result], [Error] and [ResultInfo]. The error and result-info properties are optional.
	 *
	 * <p>A response event is dropped if it doesn't have a [Request] property, or if the
	 * requestor's [Context] properties have changed since making the request.
	 */
	internal class EventHandler
	{
		private static const log:ILogger=LogFactory.getLogger(EventHandler);
		/**
		 * Either the whole response event is delivered (to a response handler), or
		 * [request, result] is delivered.
		 */
		private static const RESPONSE_EVENT_ARG_INDEX:int=0;
		private static const REQUEST_EVENT_ARG_INDEX:int=0;
		private static const RESULT_ARG_INDEX:int=1;
		private static const ERROR_ARG_INDEX:int=2;
//		private static const RESPONSE_INFO_SENTINEL:ResponseInfo=new ResponseInfo();
		private var _targetErrorHandlers:Array /*Function*/=[];

		//-------------------------------------------------------
		// Property: target

		private var _target:Object;

		/** The owner of the event listener/callback/handler. */
		public function get target():Object
		{
			return _target;
		}

		/** @private */
		public function set target(value:Object):void
		{
			_target=value;

			var errHandlerInfos:Array /*TaggedMethodInfo*/=ReflectionUtil.getTaggedMethods(_target, Constants.TAG_ERROR_HANDLER);
			for each (var methodInfo:TaggedMethodInfo in errHandlerInfos)
			{
				var errorHandler:Function=_target[methodInfo.name];
				_targetErrorHandlers.push(errorHandler);
			}
		}

		//-------------------------------------------------------
		/** The callback or handler for the event. */
		public var listener:Function;

		/** Metadata for the handler method or property. */
		public var handlerInfo:TaggedMemberInfo;

		/**
		 * The pending-request registry that is to be used to lookup information on the events
		 * that get dispatched through this functor.
		 */
		public var pendingRequestRegistry:PendingRequestRegistry;

		/**
		 * Indicates whether the listener is a [ResponseHandler] (as opposed to an
		 * [EventHandler]).
		 */
		public var isResponseHandler:Boolean=false;

		/**
		 * Indicates that if the event is a response to some earlier event, then the listener
		 * should be invoked only if the original event was raised by the same object (i.e.,
		 * the target of the listener). The default value is true.
		 */
		public var isReflexiveOnly:Boolean=true;

		/**
		 * This method acts as the delegate for the listener (for the purposes of event
		 * notifications).
		 */
		public final function onEvent(event:Event):void
		{
			if (this.handlerInfo == null)
			{
				return;
			}
			var responseInfo:ResponseInfo=this.pendingRequestRegistry.getResponseInfo(event);
			if (responseInfo == null)
			{
				// Create a "sentinel" (or empty) info object.
				responseInfo=RESPONSE_INFO_SENTINEL;
			}
			if (isDiscardableResponse(event, responseInfo))
			{
				return;
			}
			var argVals:Array /*?*/=getListenerInvocationArgs(event, responseInfo);
			notifyTarget(event, argVals, responseInfo);
		}

		/**
		 * Invokes the error and/or event/response handlers on the target object, given the
		 * event and the ordered form and the structured form of the handler arguments. argVals
		 * can be null if the response handler is a property. responseInfo applies to the cases
		 * where the event is a response (to some request).
		 *
		 * <p>Invocation is skipped if certain conditions don't meet, e.g., if the event is a
		 * response to an obsolete request.
		 */
		private function notifyTarget(event:Event, argVals:Array /*?*/, responseInfo:ResponseInfo):void
		{
			var argTypeToValidate:Class;
			var argValToValidate:Object;
			var methodArgTypes:Array /*Class*/=this.argTypes;
			if (methodArgTypes.length > 0)
			{
				argTypeToValidate=methodArgTypes[0];
				argValToValidate=argVals[0];
			}
			else if (this.handlerInfo is TaggedPropertyInfo)
			{
				argTypeToValidate=TaggedPropertyInfo(this.handlerInfo).propertyType;
				argValToValidate=responseInfo.result;
			}
			if (argTypeToValidate != null && argValToValidate != null && !(argValToValidate is argTypeToValidate))
			{
				// We only log the incompatibility of the primary arg, since that signals that
				// the same event id is being used with different event types, which can happen,
				// e.g., if they come from different modules that were written independently
				// It's not necessarily a bug, hence we don't assert or alert. But an
				// informational message could be helpful.
				var msg:String=StringUtil.substitute("Skipped invocation of {0}.{1} due to type mismatch", handlerInfo.declaringType, handlerInfo.name);
				log.warn(msg);
				return;
			}
			invokeErrorHandlerIfAny(responseInfo);
			if (!shouldInvokeEventListener(responseInfo))
			{
				return;
			}
			if (this.responseHandlerIsProperty)
			{
				target[this.handlerInfo.name]=responseInfo.result;
				return;
			}
			if (this.argTypes.length == 0)
			{
				// The event handler has no args.
				listener.apply(_target);
				return;
			}
			if (!MixedUtil.validateObjectTypes(argVals, methodArgTypes))
			{
				// Mismatch of the (additional) args, e.g. result or error, merely means that the
				// handler is not supposed to be invoked. (These are args in addition to the
				// first arg which is always an event, mismatch of which is handled earlier by
				// logging a message.) In other words, the types declared on the method are
				// meant to cause filtering (out) of the event notifications (that are not
				// applicable). The client is supposed to know what type of result or error they
				// should expect, or can deal with.
				return;
			}
			listener.apply(_target, argVals);
			onEventDelivered(event, argVals, responseInfo);
		}

		/**
		 * Performs any post-processing that may be needed once a listener has been notified
		 * of the event, given the list of arguments that were delivered to the handler and the
		 * ResponseInfo corresponding to the event.
		 */
		private function onEventDelivered(event:Event, argVals:Array /*?*/, responseInfo:ResponseInfo):void
		{
			// If the error was notified, mark it in the response-info as delievered.
			if (responseInfo.error == null)
			{
				return;
			}
			if (argVals.indexOf(event) == -1 && argVals.indexOf(responseInfo.error) == -1)
			{
				// Neither the event (which includes the error) nor the error itself is found in
				// the args that were delivered.
				return;
			}
			responseInfo.wasErrorDelivered=true;
		}

		private function get responseHandlerIsProperty():Boolean
		{
			return this.handlerInfo is TaggedPropertyInfo;
		}

		private function invokeErrorHandlerIfAny(responseInfo:ResponseInfo):void
		{
			if (_targetErrorHandlers.length == 0)
			{
				return;
			}
			var error:Error=responseInfo.error;
			if (error == null)
			{
				return;
			}
			for each (var handler:Function in _targetErrorHandlers)
			{
				handler(error);
			}
			responseInfo.wasErrorDelivered=true;
		}

		/**
		 * Event listener here means either an event/response handler in the method form or a
		 * property for the [Result] part of the event.
		 */
		private function shouldInvokeEventListener(responseInfo:ResponseInfo):Boolean
		{
			// If no error, the event listener has to be invoked (even if the result is null).
			if (responseInfo.error == null)
			{
				return true;
			}

			// If the listener is expecting more than just the result
			// and/or the error, it has to be invoked. (OR)
			// If the listener has just one argument, in other words it is expecting
			// the complete response event, then it has to be invoked.
			var argCount:int=this.argTypes.length;
			if (argCount == RESPONSE_EVENT_ARG_INDEX + 1 || argCount > ERROR_ARG_INDEX)
			{
				return true;
			}

			// By default, if there is an error, and the event listener has only
			// subscribed for the result, then we ignore partial results.
			return false;
		}

		private function isDiscardableResponse(event:Event, responseInfo:ResponseInfo):Boolean
		{
			if (responseInfo.request == null)
			{
				// It's not a response style event (because there's no associated request).
				return false;
			}
			if (!this.isReflexiveOnly)
			{
				// If it's not a reflexive-only listener, it's interested in all events.
				return false;
			}
			if (responseInfo.requestor != _target)
			{
				// The request event wasn't raised by our target, and "reflexive-only"
				// restriction was specified, so we should indicate that the event is not
				// relevant for this handler.
				return true;
			}
			var isResponseStillApplicable:Boolean=this.pendingRequestRegistry.isResponseStillApplicable(event);
			if (!isResponseStillApplicable)
			{
				// 'reflexive'-only listeners are invoked only if the response is still
				// applicable (it may become inapplicable e.g., if the context of the
				// requestor object has changed since the time when request was made).
				return true;
			}
			return false;
		}

		/**
		 * Returns the array of argument values for the invocation of the listener or handler,
		 * given an event that potentially triggers the handler. Returns an empty array if the
		 * event doesn't apply to the listener. Also returns empty if the response handler is a
		 * property (as opposed to a method).
		 */
		private function getListenerInvocationArgs(ev:Event, responseInfo:ResponseInfo):Array /*?*/
		{
			if (this.handlerInfo is TaggedPropertyInfo)
			{
				return [];
			}
			if (!this.isResponseHandler)
			{
				// The plain old [EventHandler] case: listener takes just the event itself as
				// the arg.
				return [ev];
			}
			// The [ResponseHandler] case: listener is a function that takes upto 4 args:
			// [response] or [request, result, error?, resultInfo?].

			var argCount:int=this.argTypes.length;
			if (argCount == RESPONSE_EVENT_ARG_INDEX + 1)
			{
				return [ev];
			}
			if (argCount == RESULT_ARG_INDEX + 1)
			{
				return [responseInfo.request, responseInfo.result];
			}
			if (argCount == ERROR_ARG_INDEX + 1)
			{
				return [responseInfo.request, responseInfo.result, responseInfo.error];
			}
			return [responseInfo.request, responseInfo.result, responseInfo.error, responseInfo.resultInfo];
		}

		private function get argTypes():Array /*Class*/
		{
			var methodInfo:TaggedMethodInfo=this.handlerInfo as TaggedMethodInfo;
			if (methodInfo == null)
			{
				return [];
			}
			return methodInfo.argTypes;
		}
	} // class

}